/* eslint-disable */
import { Editor, getText, getTextSerializersFromSchema, posToDOMRect } from '@tiptap/core';
import { Node as ProseMirrorNode } from '@tiptap/pm/model';
import { EditorState, Plugin, PluginKey } from '@tiptap/pm/state';
import { EditorView } from '@tiptap/pm/view';
import tippy, { Instance, Props } from 'tippy.js';

export class FloatingMenuView {
  constructor({ editor, element, view, tippyOptions = {}, shouldShow }) {
    this.editor = editor;
    this.element = element;
    this.view = view;
    this.preventHide = false;
    this.tippy = undefined;
    this.tippyOptions = tippyOptions;

    if (shouldShow) {
      this.shouldShow = shouldShow;
    } else {
      this.shouldShow = ({ view, state }) => {
        const { selection } = state;
        const { $anchor, empty } = selection;
        const isRootDepth = $anchor.depth === 1;
        const isEmptyTextBlock =
          $anchor.parent.isTextblock &&
          !$anchor.parent.type.spec.code &&
          !$anchor.parent.textContent &&
          $anchor.parent.childCount === 0;

        return !(
          !view.hasFocus() ||
          !empty ||
          !isRootDepth ||
          !isEmptyTextBlock ||
          !this.editor.isEditable
        );
      };
    }

    this.element.addEventListener('mousedown', this.mousedownHandler.bind(this), { capture: true });

    this.editor.on('focus', this.focusHandler.bind(this));
    this.editor.on('blur', this.blurHandler.bind(this));
    this.element.remove();
    this.element.style.visibility = 'visible';
  }

  mousedownHandler() {
    this.preventHide = true;
  }

  focusHandler() {
    /**
         这里使用 setTimeout 包裹 this.update(this.editor.view) 有几个重要原因：
            1. 确保编辑器状态更新完成 ：
            
            - 在焦点事件( focusHandler )触发时，编辑器的selection状态可能还未完全更新
            - 使用 setTimeout 将更新操作放入事件循环的下一个tick，确保所有状态变更已完成
            2. 避免同步更新问题 ：
            
            - 直接同步调用可能导致获取的是更新前的selection状态
            - 异步执行可以确保获取到最新的编辑器状态
            3. 性能优化 ：
            
            - 防止在密集的焦点变化事件中频繁触发更新
            - 通过异步执行可以合并快速连续的更新请求
            4. 与ProseMirror内部机制协调 ：
            
            - ProseMirror的state更新是异步的
            - 使用 setTimeout 可以与其内部更新周期保持同步
         */
    setTimeout(() => this.update(this.editor.view));
  }

  blurHandler({ event }) {
    if (this.preventHide) {
      this.preventHide = false;
      return;
    }
    if (event?.relatedTarget && this.element.parentNode?.contains(event.relatedTarget)) {
      return;
    }
    if (event?.relatedTarget === this.editor.view.dom) {
      return;
    }
    this.hide();
  }

  tippyBlurHandler(event) {
    this.blurHandler({ event });
  }

  createTooltip() {
    const editorElement = this.editor.options.element;
    const editorIsAttached = !!editorElement.parentElement;

    if (this.tippy || !editorIsAttached) return;

    this.tippy = tippy(editorElement, {
      duration: 0,
      getReferenceClientRect: null,
      content: this.element,
      interactive: true,
      trigger: 'manual',
      placement: 'right',
      hideOnClick: 'toggle',
      ...this.tippyOptions,
    });

    if (this.tippy.popper.firstChild) {
      this.tippy.popper.firstChild.addEventListener('blur', this.tippyBlurHandler.bind(this));
    }
  }

  update(view, oldState) {
    const { state } = view;
    const { doc, selection } = state;
    const { from, to } = selection;
    const isSame = oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection);

    if (isSame) return;

    this.createTooltip();

    const shouldShow = this.shouldShow?.({
      editor: this.editor,
      view,
      state,
      oldState,
    });

    if (!shouldShow) {
      this.hide();
      return;
    }

    this.tippy?.setProps({
      getReferenceClientRect:
        this.tippyOptions?.getReferenceClientRect || (() => posToDOMRect(view, from, to)),
    });

    this.show();
  }

  show() {
    this.tippy?.show();
  }

  hide() {
    this.tippy?.hide();
  }

  destroy() {
    if (this.tippy?.popper.firstChild) {
      this.tippy.popper.firstChild.removeEventListener('blur', this.tippyBlurHandler);
    }
    this.tippy?.destroy();
    this.element.removeEventListener('mousedown', this.mousedownHandler);
    this.editor.off('focus', this.focusHandler);
    this.editor.off('blur', this.blurHandler);
  }
}

export function FloatingMenuPlugin(options) {
  return new Plugin({
    key:
      typeof options.pluginKey === 'string' ? new PluginKey(options.pluginKey) : options.pluginKey,
    view: (view) => new FloatingMenuView({ view, ...options }),
  });
}
